// ### 1. 数据库与表结构

// - DB 名称：`yjs-docs`
// - 版本：`2`
// - 表：1
//   - `docs`（keyPath: `documentId`）
//     - `documentId`: string
//     - `snapshot`: ArrayBuffer（最近一次完整状态快照；若加密则配合 `iv` 字段）
//     - `iv?`: ArrayBuffer（使用 AES-GCM 时保存随机 IV）
//     - `updatedAt`: number
// 表:2
//   - `updates`（keyPath: `id`, index: `documentId_idx`）
//     - `id`: auto-increment
//     - `documentId`: string
//     - `update`: ArrayBuffer（Yjs 增量 Update；若加密则配合 `iv` 字段）
//     - `iv?`: ArrayBuffer（使用 AES-GCM 时保存随机 IV）
//     - `createdAt`: number
// 表:3
//   - `meta`（keyPath: `documentId`）
//     - `documentId`: string
//     - `lastSyncedId?`: number（已被服务端确认到的最后一条本地更新 id）
//     - `updatedAt`: number

// 设计成 增量+ 快照的形式
// 原因与价值（Snapshot + Updates）
// 快速恢复：先加载最近快照，再回放少量“未合并增量”，比从零回放全部历史快很多。
// 离线可写与断点续传：updates 是本地“操作日志”，可用 lastSyncedId 只上报未同步增量；网络恢复时续传更稳。
// 冲突友好（Yjs / CRDT）：远端 / 本地都以增量为单位合并，天然适配 Yjs 的 applyUpdate / merge 模式。
// 崩溃不丢数据：快照 + 未刷新的 updates 都在，本地可重放恢复到崩溃前状态。
// 多标签页 / 多进程可协作：不同页面追加 updates，借助 BroadcastChannel 同步通知，最终一致。
// 可控体积与性能：定期“快照压缩”（把一段 updates 合并入新快照）并截断旧 updates，避免历史无限膨胀。
// 便于审计 / 回滚（可选）：保留一定长度的 updates 可用于诊断或构建撤销链路。
// 如果不这么设计会怎样
// 只有快照
// 每次编辑都重写整快照，写放大严重；无法做“只传未同步增量”；离线时对账粒度粗、容易冲突或覆盖。
// 只有增量
// 冷启动必须回放全历史，文档越老启动越慢；日志无限增长、恢复耗时和内存持续上升，最终拖垮体验。


/**
 * IndexedDB 封装：快照/增量/清理（支持可选加密）
 */
// 自动持久化 Y.Doc：监听 doc 的 update，增量入库；页面启动时自动从 IndexedDB 恢复。
import { decryptArrayBuffer, encryptArrayBuffer, toTightArrayBuffer } from "./crypto";

const DB_NAME = "yjs-docs";
// 升级说明：v2 新增 meta 表（lastSyncedId 等），并改进读取逻辑自动按 iv 解密
const DB_VERSION = 2;
const STORE_DOCS = "docs";       // { documentId, snapshot(ArrayBuffer|Encrypted), iv?, updatedAt }
const STORE_UPDATES = "updates";  // { id(auto), documentId, update(ArrayBuffer|Encrypted), iv?, createdAt }
const STORE_META = "meta";        // { documentId, lastSyncedId?: number, updatedAt }

/** 打开数据库（含升级建表）
 * - onupgradeneeded 中创建两张表：
 *   - docs：存储每个文档的“最近一次完整状态快照”和时间戳；
 *   - updates：存储增量更新列表，并建立 documentId 索引便于查询。
 */
export function openDB(): Promise<IDBDatabase> {
  return new Promise((resolve, reject) => {
    const req = indexedDB.open(DB_NAME, DB_VERSION);
    req.onupgradeneeded = () => {
      const db = req.result;
      if (!db.objectStoreNames.contains(STORE_DOCS)) {
        db.createObjectStore(STORE_DOCS, { keyPath: "documentId" });
      }
      if (!db.objectStoreNames.contains(STORE_UPDATES)) {
        const s = db.createObjectStore(STORE_UPDATES, { keyPath: "id", autoIncrement: true });
        s.createIndex("documentId_idx", "documentId", { unique: false });
      }
      // v2: 元数据表，用于记录 lastSyncedId / 其他标记
      if (!db.objectStoreNames.contains(STORE_META)) {
        db.createObjectStore(STORE_META, { keyPath: "documentId" });
      }
    };
    req.onsuccess = () => resolve(req.result);
    req.onerror = () => reject(req.error);
  });
}

/** 保存快照（可选加密）
 * @param documentId 业务文档 ID
 * @param snapshot   由 Y.encodeStateAsUpdate(doc) 生成的全量更新包
 * @param encrypt    是否加密（默认 false）
 */
export async function saveSnapshot(documentId: string, snapshot: Uint8Array, encrypt = false): Promise<void> {
  const db = await openDB();
  return new Promise<void>(async (resolve, reject) => {
    const tx = db.transaction(STORE_DOCS, "readwrite");
    const store = tx.objectStore(STORE_DOCS);

    try {
      let payload: any = { documentId, updatedAt: Date.now() };
      if (encrypt) {
        const { iv, data } = await encryptArrayBuffer(toTightArrayBuffer(snapshot));
        payload.snapshot = data;
        payload.iv = iv.buffer;
      } else {
        payload.snapshot = toTightArrayBuffer(snapshot);
      }
      store.put(payload);
    } catch (e) {
      tx.abort();
      return reject(e);
    }

    tx.oncomplete = () => resolve();
    tx.onerror = () => reject(tx.error);
  });
}

/** 读取快照（自动解密）
 * @param documentId 文档 ID
 * @param encrypted  数据是否已加密（与写入时一致）
 */
// 自动按是否存在 iv 解密；不再依赖外部传入的 encrypted 标志
export async function getSnapshot(documentId: string): Promise<Uint8Array | null> {
  const db = await openDB();
  return new Promise((resolve, reject) => {
    const tx = db.transaction(STORE_DOCS, "readonly");
    const req = tx.objectStore(STORE_DOCS).get(documentId);
    req.onsuccess = async () => {
      const row = req.result as any;
      if (!row?.snapshot) return resolve(null);
      try {
        if (row.iv) {
          const plain = await decryptArrayBuffer(new Uint8Array(row.iv), row.snapshot);
          resolve(new Uint8Array(plain));
        } else {
          resolve(new Uint8Array(row.snapshot));
        }
      } catch (e) {
        return reject(e);
      }
    };
    req.onerror = () => reject(req.error);
  });
}

/** 追加一条增量（可选加密）
 * @param documentId 文档 ID
 * @param update     ydoc.on('update') 拿到的一次增量
 * @param encrypt    是否加密
 */
export async function addUpdate(documentId: string, update: Uint8Array, encrypt = false): Promise<void> {
  const db = await openDB();
  return new Promise<void>(async (resolve, reject) => {
    const tx = db.transaction(STORE_UPDATES, "readwrite");
    const store = tx.objectStore(STORE_UPDATES);
    try {
      if (encrypt) {
        const { iv, data } = await encryptArrayBuffer(toTightArrayBuffer(update));
        store.add({ documentId, update: data, iv: iv.buffer, createdAt: Date.now() });
      } else {
        store.add({ documentId, update: toTightArrayBuffer(update), createdAt: Date.now() });
      }
    } catch (e) {
      tx.abort();
      return reject(e);
    }
    tx.oncomplete = () => resolve();
    tx.onerror = () => reject(tx.error);
  });
}

/** 读取该文档的所有增量（自动解密）
 * - 用于启动时本地恢复，或重连后批量上行。
 */
// 返回按 id 递增排序的更新；自动基于 iv 解密
export async function getUpdates(documentId: string): Promise<Uint8Array[]> {
  const db = await openDB();
  return new Promise((resolve, reject) => {
    const tx = db.transaction(STORE_UPDATES, "readonly");
    const idx = tx.objectStore(STORE_UPDATES).index("documentId_idx");
    const req = idx.getAll(IDBKeyRange.only(documentId));
    req.onsuccess = async () => {
      let rows: any[] = req.result || [];
      rows.sort((a: any, b: any) => a.id - b.id);
      try {
        const out: Uint8Array[] = [];
        for (const r of rows) {
          if (r.iv) {
            const plain = await decryptArrayBuffer(new Uint8Array(r.iv), r.update);
            out.push(new Uint8Array(plain));
          } else {
            out.push(new Uint8Array(r.update));
          }
        }
        resolve(out);
      } catch (e) {
        reject(e);
      }
    };
    req.onerror = () => reject(req.error);
  });
}

/** 清理旧增量：保留最近 keepLast 条，或按 maxAgeMs 清理过期
 * - 建议每次保存快照后调用，防止本地存储无限增长。
 */
export async function cleanupOldUpdates(documentId: string, opt: { keepLast?: number; maxAgeMs?: number }): Promise<void> {
  const db = await openDB();
  const { keepLast = 1000, maxAgeMs } = opt || {};
  const now = Date.now();
  return new Promise((resolve, reject) => {
    const tx = db.transaction(STORE_UPDATES, "readwrite");
    const idx = tx.objectStore(STORE_UPDATES).index("documentId_idx");
    const req = idx.getAll(IDBKeyRange.only(documentId));
    req.onsuccess = () => {
      const all = (req.result as any[]) || [];
      all.sort((a: any, b: any) => a.id - b.id);
      const toDeleteA = all.slice(0, Math.max(0, all.length - keepLast));
      const toDeleteB = maxAgeMs ? all.filter((r: any) => now - r.createdAt > maxAgeMs) : [];
      const ids = new Set<number>([...toDeleteA, ...toDeleteB].map((r: any) => r.id));
      const store = tx.objectStore(STORE_UPDATES);
      ids.forEach(id => store.delete(id));
    };
    tx.oncomplete = () => resolve();
    tx.onerror = () => reject(tx.error);
  });
}

/**
 * 获取“某个 id 之后”的增量（用于重连后补发）。
 * 返回包含 id，方便推进游标或删除。
 */
export async function getUpdatesSinceId(
  documentId: string,
  sinceIdExclusive: number
): Promise<Array<{ id: number; update: Uint8Array }>> {
  const db = await openDB();
  return new Promise((resolve, reject) => {
    const tx = db.transaction(STORE_UPDATES, "readonly");
    const idx = tx.objectStore(STORE_UPDATES).index("documentId_idx");
    const req = idx.getAll(IDBKeyRange.only(documentId));
    req.onsuccess = async () => {
      let rows: any[] = req.result || [];
      rows = rows.filter(r => typeof r.id === 'number' && r.id > sinceIdExclusive);
      rows.sort((a: any, b: any) => a.id - b.id);
      try {
        const out: Array<{ id: number; update: Uint8Array }> = [];
        for (const r of rows) {
          if (r.iv) {
            const plain = await decryptArrayBuffer(new Uint8Array(r.iv), r.update);
            out.push({ id: r.id, update: new Uint8Array(plain) });
          } else {
            out.push({ id: r.id, update: new Uint8Array(r.update) });
          }
        }
        resolve(out);
      } catch (e) {
        reject(e);
      }
    };
    req.onerror = () => reject(req.error);
  });
}

/**
 * 读取/设置“最后一次与服务端确认的更新 id”（上行游标）。
 */
export async function getLastSyncedId(documentId: string): Promise<number> {
  const db = await openDB();
  return new Promise((resolve, reject) => {
    const tx = db.transaction(STORE_META, "readonly");
    const req = tx.objectStore(STORE_META).get(documentId);
    req.onsuccess = () => {
      const row = (req.result as any) || null;
      if (row && typeof row.lastSyncedId === 'number') {
        resolve(row.lastSyncedId);
      } else {
        // 懒初始化：首次读取即创建默认游标
        const wtx = db.transaction(STORE_META, "readwrite");
        const store = wtx.objectStore(STORE_META);
        store.put({ documentId, lastSyncedId: 0, updatedAt: Date.now() });
        wtx.oncomplete = () => resolve(0);
        wtx.onerror = () => reject(wtx.error);
      }
    };
    req.onerror = () => reject(req.error);
  });
}

export async function setLastSyncedId(documentId: string, lastSyncedId: number): Promise<void> {
  const db = await openDB();
  return new Promise((resolve, reject) => {
    const tx = db.transaction(STORE_META, "readwrite");
    const store = tx.objectStore(STORE_META);
    store.put({ documentId, lastSyncedId, updatedAt: Date.now() });
    tx.oncomplete = () => resolve();
    tx.onerror = () => reject(tx.error);
  });
}

/**
 * 删除“某 id 及之前”的增量（用于服务端确认后本地清理）。
 */
export async function deleteUpdatesUpToId(documentId: string, upToIdInclusive: number): Promise<void> {
  const db = await openDB();
  return new Promise((resolve, reject) => {
    const tx = db.transaction(STORE_UPDATES, "readwrite");
    const idx = tx.objectStore(STORE_UPDATES).index("documentId_idx");
    const req = idx.getAll(IDBKeyRange.only(documentId));
    req.onsuccess = () => {
      const rows: any[] = req.result || [];
      const store = tx.objectStore(STORE_UPDATES);
      for (const r of rows) {
        if (typeof r.id === 'number' && r.id <= upToIdInclusive) {
          store.delete(r.id);
        }
      }
    };
    tx.oncomplete = () => resolve();
    tx.onerror = () => reject(tx.error);
  });
}


